package com.xiyuan.smartutils.client.http;

import com.xiyuan.smartutils.*;
import com.xiyuan.smartutils.crypto.SSL.DefaultHostnameVerifier;
import com.xiyuan.smartutils.crypto.SSL.DefaultTrustManager;
import com.xiyuan.smartutils.constants.HttpConstants;
import com.xiyuan.smartutils.control.ExceptionHandler;
import com.xiyuan.smartutils.logging.LogFactory;
import com.xiyuan.smartutils.logging.Logger;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Http客户端基类，实现子类实现execute功能
 *
 * @version v1.0.0 @author lgz 2017-9-21 新建与整理
 * @see HttpGet GET方法调用，只需URL，无需传入参数
 * @see HttpPost POST方法调用，传入Form参数
 * @see HttpDownload GET方法调用，得到内容是大文件，保存到本地
 * @see HttpUpload POST方法调用，传入Form-data，包括文件和参数
 */
@SuppressWarnings("all")
public class HttpClient implements HttpConstants {
    public static final String _USER_AGENT_VALUE_ = "HttpClient/V1.0.0";
    public static final Pattern _REGEX_FILE_NAME_ = Pattern.compile("attachment;\\s*filename=\"([\\w\\-\\._]+)\"");
    
    /**************************************************************************/
    // 以下为HTTP参数
    /**************************************************************************/
    
    /**
     * The log.
     */
    private static final Logger log = LogFactory.getLogger(HttpClient.class);
    
    /**
     * 连接超时时间，单位秒，默认10秒，实现传入毫秒，=0表示一直等待直到成功，!=0时时间达到则抛出java.net.SocketTimeoutException
     */
    private int connectTimeout = 10;
    
    /***
     * 读资源超时时间，单位秒，默认3600秒，实现传入毫秒，=0表示一直等待直到成功，!=0时时间达到则抛出java.net.SocketTimeoutException
     */
    private int readTimeout = 1800;
    
    /**
     * 是否输入，默认=true
     */
    private boolean doInput = true;
    
    /**
     * 是否输出，默认=false
     */
    private boolean doOutput = false;
    
    /**
     * 当3XX重定向时，是否访问重定向的地址，HTTP默认是true，我们改为默认false，即返回location
     */
    private boolean instanceFollowRedirects = false;
    
    
    /**
     * 设置当前是否使用缓存，我们默认=false，系统默认取defaultUseCaches=true，也可以实际设置
     */
    private boolean useCaches = false;
    
    /**
     * // 设置资源比较时间
     */
    private long ifModifiedSince = 0;
    
    /**
     * 指定资源块大小，系统默认-1，提供了一个缺省值4096参考，如果未设置，则缓存本地再发送，否则流逐步发送，
     */
    private int chunkLength = -1; //
    
    /**
     * 指定读取资源内容大小，默认是未开启，按服务端ContentLength读内容大小，只有设置了才有效，默认-1
     */
    private int fixedContentLength = -1;
    
    
    /**
     * 允许用户交互，默认=false
     */
    private boolean allowUserInteraction = false;
    
    /**
     * 是否有客户端代理
     */
    private boolean hasUserAgent = false;
    
    private boolean isPrintException = true;
    private ExceptionHandler exceptionHandler;
    
    
    // 请求属性参数表
    private final HttpHeader reqHeaderMap = new HttpHeader();
    
    // 响应属性参数表
    private final HttpHeader rspHeaderMap = new HttpHeader();
    // 响应状态行//
    private String headerLine;
    
    // 请求连接
    protected String url;
    
    // HTTP方法 [GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE]
    private String method;
    // 信任管理器
    private X509TrustManager trustManager;
    
    // 响应状态码
    protected int responseStatus;
    // 响应状态信息
    protected String responseText;
    private HttpResult result;
    
    public HttpClient(String url, String method) {
        this.url = url;
        this.method = method;
    }
    
    @SuppressWarnings("unchecked")
    private <T> T self() {
        return (T) this;
    }
    /***********************************************************/
    // 通用的执行方法，子类不可重写，子类重写以下三个方法
    
    /***********************************************************/
    public final <T extends HttpClient> T execute() {
        if (!isValidUrl()) {return self();}
        
        HttpURLConnection conn = null;
        Exception ex = null;
        try {
            // 1.预置请求连接属性和消息头信息，如果返回false表示不可执行
            if (!doPreRequestProperty()) {return self();}
            
            // 2.新建连接并连接
            conn = newHttpConnection();
            conn.connect();
            
            // 3.子类可重写该方法设置连接内容
            doWriteContent(conn);
            
            // 4.获取响应状态和响应属性
            responseStatus = conn.getResponseCode();
            doReadHeader(conn);
            
            // 5.子类可重写该方法获取连接内容
            doReadContent(conn);
        }
        catch (SocketTimeoutException e) {
            responseStatus = _73_SOCKET_TIMEOUT_.getCode();
            responseText = "调用服务端超时";
            ex = e;
        }
        catch (ConnectException e) {
            responseStatus = _91_CONNECT_EXCEPTION_.getCode();
            responseText = "连接服务器失败";
            ex = e;
        }
        catch (SocketException e) {
            responseStatus = _90_SOCKET_EXCEPTION_.getCode();
            responseText = "调用服务端失败:" + e.getMessage();
            ex = e;
        }
        catch (IOException e) {
            responseStatus = _98_IO_EXCEPTION_.getCode();
            responseText = "调用服务端失败:" + e.getMessage();
            ex = e;
        }
        catch (Throwable e) {
            responseStatus = _99_EXCEPTION_.getCode();
            responseText = "调用服务端异常:" + e.getMessage();
            ex = e instanceof Exception ? (Exception) e : new Exception(e.getMessage());
            ex.setStackTrace(e.getStackTrace());
        }
        finally {
            this.result = new HttpResult(responseStatus, responseText).setException(ex);
            if (conn != null) {conn.disconnect();}
            doPrintException(ex);
        }
        
        return self();
    }
    
    private void doPrintException(Throwable ex) {
        if (ex == null) {return;}
        if (exceptionHandler != null) {
            exceptionHandler.handle(ex);
        }
        else {
            if (!isPrintException) {return;}
            log.error(responseText, ex);
        }
    }
    
    public final boolean isPrintException() {return isPrintException;}
    
    public final <T> T setExceptionHandler(ExceptionHandler exceptionHandler) {
        this.exceptionHandler = exceptionHandler;
        return self();
    }
    
    /***
     * 设置当异常时是否输出异常，
     * @param isPrintException 是否输出异常
     * @return this
     */
    public final <T extends HttpClient> T setPrintException(boolean isPrintException) {
        this.isPrintException = isPrintException;
        return self();
    }
    /***********************************************************/
    // 子类可重写的方法，包括预置请求属性、写请求内容，读响应内容
    /***********************************************************/
    
    /**
     * 预处理请求属性，并检查是否可执行，默认可执行，子类重写
     *
     * @return =true表示正常可执行，=false表示异常不向下执行
     */
    protected boolean doPreRequestProperty() throws Exception {
        return true;
    }
    
    /**
     * 写请求内容，子类可重写
     *
     * @param conn HTTP/HTTPS连接
     * @throws IOException 可能的异常
     */
    protected void doWriteContent(HttpURLConnection conn) throws IOException {
    }
    
    /**
     * 读响应内容，子类可重写
     *
     * @param conn HTTP/HTTPS连接
     * @throws IOException 可能的异常
     */
    protected void doReadContent(HttpURLConnection conn) throws IOException {
        if (responseStatus == _302_FOUND_.getCode()) {
            // 重定向
            responseText = conn.getHeaderField("Location");
            return;
        }
        
        responseText = getResponseAsString(conn);
    }
    
    /*****************************************************************************/
    // 以下为创建连接，并设置基本属性
    
    /*****************************************************************************/
    
    private boolean isValidUrl() {
        if (Validates.isUrl(url)) {return true;}
        
        responseStatus = _70_MALFORMED_URL_.getCode();
        responseText = "请求的URL不正确";
        return false;
    }
    
    /**
     * 创建连接
     */
    private HttpURLConnection newHttpConnection() throws Exception {
        if (Validates.isHttp(url)) {
            // HTTP连接
            HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
            setConnectionProperty(conn);
            return conn;
        }
        else {// HTTPS连接，为支持JDK1.6，设置协议为TLSv1，JDK1.7开始有TLSv1.1,TLSv1.2
            SSLContext ctx = SSLContext.getInstance(Systems.isJavaVersionMoreThen7() ? "TLSv1.2" : "TLSv1");
            ctx.init(null,
                     new TrustManager[]{trustManager != null ? trustManager : new DefaultTrustManager()},
                     new SecureRandom());
            
            HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection();
            conn.setSSLSocketFactory(ctx.getSocketFactory());
            conn.setHostnameVerifier(new DefaultHostnameVerifier());
            setConnectionProperty(conn);
            return conn;
        }
    }
    
    /**
     * 设置连接属性
     *
     * @throws ProtocolException 异常
     */
    private void setConnectionProperty(HttpURLConnection conn) throws ProtocolException {
        // 方法
        
        conn.setRequestMethod(method);
        
        // 输入输出开关，输入默认打开，输出默认关闭
        if (!doInput) {conn.setDoInput(doInput);}
        if (doOutput) {conn.setDoOutput(doOutput);}
        
        // 当3XX重定向时，当前实例是否访问重定向的地址
        conn.setInstanceFollowRedirects(instanceFollowRedirects);
        
        // 超时设置，大于0时设置
        if (connectTimeout > 0) {conn.setConnectTimeout(connectTimeout * 1000);}
        if (readTimeout > 0) {conn.setReadTimeout(readTimeout * 1000);}
        
        // 是否启用缓存，默认开启
        if (!useCaches) {conn.setUseCaches(useCaches);}
        
        // 有设置比较资源时间
        if (ifModifiedSince > 0) {conn.setIfModifiedSince(ifModifiedSince);}
        
        // 有设置固定读资源内容大小，和设置资源块大小互斥，默认优先取固定读资源内容大小
        if (fixedContentLength != -1) {conn.setFixedLengthStreamingMode(fixedContentLength);}
        
        // 有设置资源块大小，和设置固定资源大小互斥
        if (fixedContentLength == -1 && chunkLength != -1) {conn.setChunkedStreamingMode(chunkLength);}
        
        // 有设置允许人工交互
        if (allowUserInteraction) {conn.setAllowUserInteraction(allowUserInteraction);}
        
        for (Map.Entry<String, List<String>> entry : reqHeaderMap.entrySet()) {
            String name = entry.getKey();
            List<String> vals = entry.getValue();
            vals.forEach(val -> conn.setRequestProperty(name, val));
        }
        
        // 未设置代理的，设置默认
        if (!hasUserAgent) {conn.addRequestProperty(_USER_AGENT_, _USER_AGENT_VALUE_);}
    }
    
    /***********************************************************/
    // 增加请求属性信息，包括信任管理器，属性，是否输入输出流等
    /***********************************************************/
    
    /**
     * 设置信任管理器
     */
    public void setTrustManager(X509TrustManager trustManager) {
        this.trustManager = trustManager;
    }
    
    /**
     * 设置请求属性完成后关闭
     */
    public void setConnectionClose() {
        reqHeaderMap.addHeader(_CONNECTION_, "Close");
    }
    
    /**
     * 增加请求属性
     */
    public <T extends HttpClient> T addRequestProperty(String key, long value) {
        return addRequestProperty(true, key, String.valueOf(value));
    }
    
    /**
     * 增加请求属性
     *
     * @return this instance
     */
    public <T extends HttpClient> T addRequestProperty(String key, String value) {
        return addRequestProperty(true, key, value);
    }
    
    /**
     * 增加请求属性
     */
    public <T extends HttpClient> T addRequestProperty(boolean condition, String key, Object value) {
        if (_USER_AGENT_.equalsIgnoreCase(key)) {hasUserAgent = true;}
        if (condition) {
            reqHeaderMap.addHeader(key, String.valueOf(value));
        }
        return self();
    }
    
    /**
     * 获取请求属性表
     */
    public HttpHeader getRequestHeader() {
        return reqHeaderMap;
    }
    
    /**
     * 获取请求属性
     */
    public final String getRequestHeader(String key) {
        return reqHeaderMap.getHeader(key);
    }
    
    /**
     * 删除请求头信息
     *
     * @param key key
     * @return 被删除的内容
     */
    public final String removeRequestHeader(String key) {
        return reqHeaderMap.removeHeader(key);
    }
    
    /**
     * 是否有请求属性
     */
    public final boolean hasRequestHeader(String key) {
        return reqHeaderMap.containsHeader(key);
    }
    
    /**
     * 获取配置的URL
     */
    public String getUrl() {
        return url;
    }
    
    /**
     * 获取方法
     */
    public String getMethod() {
        return method;
    }
    
    /**
     * 判断是否打开了输入流
     */
    public boolean isDoInput() {
        return doInput;
    }
    
    /**
     * 设置是是否有输入流
     */
    public void setDoInput(boolean doInput) {
        this.doInput = doInput;
    }
    
    /**
     * 判断是否打开了输出流
     */
    public boolean isDoOutput() {
        return doOutput;
    }
    
    /**
     * 设置是是否有输出流
     */
    public void setDoOutput(boolean doOutput) {
        this.doOutput = doOutput;
    }
    
    /**
     * 设置连接超时时长，单位：秒
     */
    public <T extends HttpClient> T setConnectTimeout(int connectTimeout) {
        if (connectTimeout > 1) {this.connectTimeout = connectTimeout;}
        return self();
    }
    
    /**
     * 获取连接超时时长，单位：秒
     */
    public int getConnectTimeout() {
        return connectTimeout;
    }
    
    /**
     * 设置数据获取超时时长，单位：秒
     */
    public <T extends HttpClient> T setReadTimeout(int readTimeout) {
        if (readTimeout > 1) {this.readTimeout = readTimeout;}
        return self();
    }
    
    /**
     * 获取读流超时时长
     */
    public int getReadTimeout() {
        return readTimeout;
    }
    
    /**
     * 设置协议是否使用缓存，默认是有条件即使用，我们修改成不使用
     */
    public boolean isUseCaches() {
        return useCaches;
    }
    
    /**
     * 设置协议是否使用缓存
     */
    public void setUseCaches(boolean useCaches) {
        this.useCaches = useCaches;
    }
    
    /**
     * 获取资源本地缓存上次最后修改时间
     */
    public long getIfModifiedSince() {
        return ifModifiedSince;
    }
    
    /**
     * 设置资源本地缓存上次最后修改时间
     */
    public void setIfModifiedSince(long ifModifiedSince) {
        this.ifModifiedSince = ifModifiedSince;
    }
    
    /**
     * 获取配置的块大小，-1表示未配置，操作时默认4096
     */
    public int getChunkLength() {
        return chunkLength;
    }
    
    /**
     * 设置块大小，-1表示不生效，取默认4096
     */
    public void setChunkLength(int chunkLength) {
        this.chunkLength = chunkLength;
    }
    
    /**
     * 获取配置的读取固定内容长度，-1表示不生效，取响应的Content-Length
     */
    public int getFixedContentLength() {
        return fixedContentLength;
    }
    
    /**
     * 设置配置的读取固定内容长度，-1表示不生效，取响应的Content-Length
     */
    public void setFixedContentLength(int fixedContentLength) {
        this.fixedContentLength = fixedContentLength;
    }
    
    /**
     * 判断是否允许用户交互，用于访问服务端时等待管理操作验证
     */
    public boolean isAllowUserInteraction() {
        return allowUserInteraction;
    }
    
    /**
     * 设置是否允许用户交互，默认是不允许
     */
    public void setAllowUserInteraction(boolean allowUserInteraction) {
        this.allowUserInteraction = allowUserInteraction;
    }
    
    /**
     * 判断当3XX重定向时，是否访问重定向的地址
     */
    public boolean isInstanceFollowRedirects() {
        return instanceFollowRedirects;
    }
    
    /**
     * 设置当3XX重定向时，是否访问重定向的地址
     */
    public void setInstanceFollowRedirects(boolean instanceFollowRedirects) {
        this.instanceFollowRedirects = instanceFollowRedirects;
    }
    
    /***********************************************************/
    // 获取结果信息，包括状态、内容和字节流
    /***********************************************************/
    
    /**
     * 增加响应属性
     */
    protected void doReadHeader(URLConnection conn) {
        headerLine = conn.getHeaderField(0);
        Map<String, List<String>> map = conn.getHeaderFields();
        for (String key : map.keySet()) {
            rspHeaderMap.addHeader(key, conn.getHeaderField(key));
        }
    }
    
    /**
     * 获取响应状态
     */
    public int getResponseStatus() {
        return responseStatus;
    }
    
    /**
     * 判断响应是否成功
     */
    public boolean isOK() {
        return responseStatus == 200;
    }
    
    /**
     * 获取响应内容(正确内容或错误内容)
     */
    public String getResponseText() {
        return responseText;
    }
    
    /**
     * 返回指定的Http调用结果
     */
    public HttpResult getResult() {
        return result;
    }
    
    /**
     * 获取响应状态行
     */
    public String getResponseStatusLine() {
        return headerLine;
    }
    
    /**
     * 获取响应属性
     */
    public String getResponseHeader(String key) {
        return rspHeaderMap.getHeader(key.toLowerCase());
    }
    
    /**
     * 是否有响应属性
     */
    public boolean hasResponseHeader(String key) {
        return rspHeaderMap.containsHeader(key);
    }
    
    /**
     * 获取响应属性表
     */
    public HttpHeader getResponseHeader() {
        return rspHeaderMap;
    }
    
    /**
     * 获取响应属性列表
     */
    public List<String> getResponseNames() {
        
        return new ArrayList<String>(rspHeaderMap.getHeaderNames());
    }
    
    /**
     * 获取响应内容长度
     */
    public int getResponseContentLength() {
        String value = rspHeaderMap.getHeader(_CONTENT_LENGTH_.toLowerCase());
        if (Validates.isBlank(value)) {return 0;}
        
        value = value.trim();
        if (!Validates.isIntegerPositive(value)) {
            return 0;
        }
        
        return Ints.toInt(value);
    }
    
    /**
     * 获取响应内容类型
     */
    public String getResponseContentType() {
        return getResponseHeader(_CONTENT_TYPE_);
    }
    
    /**
     * 获取响应内容字符集，默认UTF-8
     */
    public String getResponseCharset() {
        return getResponseCharset(getResponseContentType());
    }
    
    /**
     * 获取响应内容编码
     */
    public String getResponseContentEncoding() {
        return getResponseHeader(_CONTENT_ENCODING_);
    }
    
    /**
     * 判断响应内容编码是否是Gzip
     */
    public boolean isResponseGzip() {
        return _ENCODING_GZIP_.equals(getResponseContentEncoding());
    }
    
    /*****************************************************************************/
    // 以下为静态方法
    /*****************************************************************************/
    
    /**
     * 判断是否响应使用GZIP压缩
     *
     * @param conn HTTP连接
     * @return =true表示启用，=false表示未启用
     */
    public static boolean isResponseGzip(HttpURLConnection conn) {
        return "gzip".equalsIgnoreCase(conn.getContentEncoding());
    }
    
    /**
     * 从响应中获取内容字符集
     *
     * @param contentType 内容类型
     * @return String, 默认UTF-8
     */
    public static String getResponseCharset(String contentType) {
        if (Validates.isBlank(contentType)) {return _UTF_8_;}
        
        String[] params = Arrays.toStringArray(contentType.trim(), ";");
        for (String param : params) {
            if (!param.startsWith(_CHARSET_)) {continue;}
            
            String[] pair = Arrays.toStringArray(param, "=");
            if (pair.length != 2 || Validates.isEmpty(pair[1])) {continue;}
            
            return pair[1];
        }
        
        return _UTF_8_;
    }
    
    /**
     * 从响应中获取字符串，包括成功和失败的内容
     *
     * @param conn HTTP连接
     * @return 成功或失败的内容
     * @throws IOException 可能的异常
     */
    public static String getResponseAsString(HttpURLConnection conn) throws IOException {
        boolean isGzip = isResponseGzip(conn);
        String charset = getResponseCharset(conn.getContentType());
        if (conn.getErrorStream() != null) {// 有出错返回
            String message = null;
            if (isGzip) {
                message = IoUtils.getStringGzip(conn.getErrorStream(), charset);
            }
            else {
                message = IoUtils.readString(conn.getErrorStream(), charset);
            }
            
            if (Validates.isBlank(message)) {message = "调用服务端接口失败，错误码：" + conn.getResponseCode();}
            
            return message;
        }
        else if (conn.getInputStream() != null) {// 正常返回
            if (isGzip) {
                return IoUtils.getStringGzip(conn.getInputStream(), charset);
            }
            else {
                return IoUtils.readString(conn.getInputStream(), charset);
            }
        }
        else {// 无出错内容返回
            return "调用服务端接口失败，错误码：" + conn.getResponseCode();
        }
    }
    
    /**
     * 读取错误流中的内容
     *
     * @param conn HTTP连接
     * @return 成功或失败的内容
     * @throws IOException 可能的异常
     */
    public static String getResponseError(HttpURLConnection conn) throws IOException {
        InputStream stream = conn.getErrorStream();
        if (stream == null) {return "调用服务端接口失败，错误码：" + conn.getResponseCode();}
        
        boolean isGzip = isResponseGzip(conn);
        String charset = getResponseCharset(conn.getContentType());
        String message = null;
        if (isGzip) {
            message = IoUtils.getStringGzip(stream, charset);
        }
        else {
            message = IoUtils.readString(stream, charset);
        }
        
        if (Validates.isBlank(message)) {message = "调用服务端接口失败，错误码：" + conn.getResponseCode();}
        
        return message;
    }
    
    /**
     * 通过连接获取下载名称
     *
     * @param conn HttpURLConnection
     * @return fileName
     */
    public static String getFileName(HttpURLConnection conn) {
        String contentDisposition = conn.getHeaderField(_CONTENT_DISPOSITION_);
        if (contentDisposition == null) {return null;}
        
        Matcher matcher = _REGEX_FILE_NAME_.matcher(contentDisposition);
        if (matcher.find()) {return StrUtils.trim(matcher.group(1));}
        return null;
    }
    
}
